//=============================================================================
// System  : MatchKit
// File    : ObjectMapper.cs
// Author  : Fabio Bussu  (matchkit@fabiobussu.it)
// Updated : 04/16/2013
// Note    : Copyright 2013, Fabio Bussu, All rights reserved
//
// This code is published under the Microsoft Public License (Ms-PL). A copy
// of the license should be distributed with the code.  It can also be found
// at the project website: http://matchkit.codeplex.com. This notice, the
// author's name, and all copyright notices must remain intact in all
// applications, documentation, and source files.
//
// Version     Date     Who  Comments
// ============================================================================
// 1.0.0.0  04/16/2013   FB  First draft
//=============================================================================
using System;
using System.Collections.Generic;
using MatchKit.ObjectMapping.Patterns;

namespace MatchKit.ObjectMapping
{
	/// <summary>
	/// Provides static methods to map a matchable class on a string.
	/// </summary>
	/// <conceptualLink target="0a06b2a9-5f58-409a-ba62-639cfd77bca7" />
	public static class ObjectMapper
	{
		/// <summary>
		/// Maps a generic matchable type on a string with default options.
		/// </summary>
		/// <typeparam name="T">Generic type to map. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The mapped matchable class.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static T Map<T>(string s)
		{
			return Map<T>(s, new ObjectMapperOptions());
		}

		/// <summary>
		/// Maps a generic matchable type on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to map. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The mapped matchable class.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static T Map<T>(string s, ObjectMapperOptions options)
		{
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");
			return (T)Map(typeof(T), s, options);
		}

		/// <summary>
		/// Maps a matchable instance on a string with default options.
		/// </summary>
		/// <param name="instance">Instance to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The mapped matchable class, equals to input instance.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static object Map(object instance, string s)
		{
			return Map(instance, s, new ObjectMapperOptions());
		}

		/// <summary>
		/// Maps a matchable instance on a string with specific options.
		/// </summary>
		/// <param name="instance">Instance to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The mapped matchable class, equals to input instance.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static object Map(object instance, string s, ObjectMapperOptions options)
		{
			if (instance == null)
				throw new ArgumentNullException("instance", "instance cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			// faccio questa chiamata per forzare i controlli
			try
			{
				MatchableClassInfo.Get(instance.GetType());
			}
			catch (Exception ex)
			{
				throw new ArgumentException("instance must be a valid matchable class", ex);
			}

			return Map(new ObjectPattern(instance), s, options);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with default options.
		/// </summary>
		/// <param name="instance">Instance to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(object instance, string s)
		{
			return BeginMap(instance, s, new ObjectMapperOptions(), null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <param name="instance">Instance to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(object instance, string s, ObjectMapperOptions options)
		{
			return BeginMap(instance, s, options, null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <param name="instance">Instance to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(object instance, string s, ObjectMapperOptions options, AsyncCallback callback, object obj)
		{
			if (instance == null)
				throw new ArgumentNullException("instance", "instance cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var d = new Func<ObjectPattern, string, ObjectMapperOptions, object>(Map);
			return new MapAsyncResult(d, d.BeginInvoke(new ObjectPattern(instance), s, options, callback, obj));
		}

		/// <summary>
		/// Maps a matchable type on a string with default options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The mapped matchable class.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static object Map(Type type, string s)
		{
			return Map(type, s, new ObjectMapperOptions());
		}

		/// <summary>
		/// Maps a matchable type on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The mapped matchable class.</returns>
		/// <remarks>
		/// This method ensures that the string match exactly the matchable class with the specified options.
		/// </remarks>
		public static object Map(Type type, string s, ObjectMapperOptions options)
		{
			if (type == null)
				throw new ArgumentNullException("type", "type cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			// faccio questa chiamata per forzare i controlli
			MatchableClassInfo.Get(type);

			return Map(new ObjectPattern(type), s, options);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(Type type, string s)
		{
			return BeginMap(type, s, new ObjectMapperOptions(), null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to map. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap<T>(string s)
		{
			return BeginMap(typeof(T), s, new ObjectMapperOptions(), null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(Type type, string s, ObjectMapperOptions options)
		{
			return BeginMap(type, s, options, null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to map. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap<T>(string s, ObjectMapperOptions options)
		{
			return BeginMap(typeof(T), s, options, null, null);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to map. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap<T>(string s, ObjectMapperOptions options, AsyncCallback callback, object obj)
		{
			return BeginMap(typeof(T), s, options, callback, obj);
		}

		/// <summary>
		/// Performs an asynchronous map on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginMap(Type type, string s, ObjectMapperOptions options, AsyncCallback callback, object obj)
		{
			if (type == null)
				throw new ArgumentNullException("type", "type cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var d = new Func<ObjectPattern, string, ObjectMapperOptions, object>(Map);
			return new MapAsyncResult(d, d.BeginInvoke(new ObjectPattern(type), s, options, callback, obj));
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Map operation.
		/// </summary>
		/// <param name="result">The async object returned by the <b>BeginMap</b> method.</param>
		/// <returns>The mapped instance.</returns>
		public static object EndMap(IAsyncResult result)
		{
			var mapAsyncResult = result as MapAsyncResult;
			if (mapAsyncResult == null)
				throw new ArgumentException("Invalid async result", "result");
			return mapAsyncResult.EndMap();
		}

		private static object Map(ObjectPattern pattern, string s, ObjectMapperOptions options)
		{
			var context = GetContext(options, s, pattern);

			var m = pattern.Match(context);
			if (!m.Success)
				throw new MatchFailedException(0, m, "Pattern mapping failed");

			return m.Value;
		}

		private static ObjectMappingContext GetContext(ObjectMapperOptions options, string s, ObjectPattern pattern)
		{
			var moptions = new MatchOptions();

			if (options != null)
			{
				moptions.CancellationFlag = options.CancellationFlag;
				moptions.Timeout = options.Timeout;
				moptions.Tracer = options.Tracer;
			}

			var context = new ObjectMappingContext(s, moptions);

			if (options != null)
			{
				context.IgnoreBlanks = options.IgnoreBlanks;
				context.MatchingBag = options.MatchBag;
				context.NullPattern = options.NullPattern;

				pattern.EndsWithEos = options.EndsWithEos;
			}

			return context;
		}

		/// <summary>
		/// Finds a generic matchable type on a string with default options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The enumerable matched classes.</returns>
		/// <remarks>
		/// This method tries to match multiple times the matchable class with the specified options inside the string.
		/// </remarks>
		public static IEnumerable<T> Find<T>(string s)
		{
			return Find<T>(s, new ObjectMapperOptions());
		}

		/// <summary>
		/// Finds a generic matchable type on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The enumerable matched classes.</returns>
		/// <remarks>
		/// This method tries to match multiple times the matchable class with the specified options inside the string.
		/// </remarks>
		public static IEnumerable<T> Find<T>(string s, ObjectMapperOptions options)
		{
			return Find<T>(typeof(T), s, options);
		}

		/// <summary>
		/// Finds a matchable type on a string with default options.
		/// </summary>
		/// <param name="type">Type to find. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The enumerable matched classes.</returns>
		/// <remarks>
		/// This method tries to match multiple times the matchable class with the specified options inside the string.
		/// </remarks>
		public static IEnumerable<object> Find(Type type, string s)
		{
			return Find(type, s, new ObjectMapperOptions());
		}

		/// <summary>
		/// Finds a matchable type on a string with specific options.
		/// </summary>
		/// <param name="type">Type to find. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The enumerable matched classes.</returns>
		/// <remarks>
		/// This method tries to match multiple times the matchable class with the specified options inside the string.
		/// </remarks>
		public static IEnumerable<object> Find(Type type, string s, ObjectMapperOptions options)
		{
			if (type == null)
				throw new ArgumentNullException("type", "type cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("settings", "settings cannot be null");
			return Find<object>(type, s, options);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with default options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(Type type, string s)
		{
			return BeginFind(type, s, new ObjectMapperOptions(), null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with default options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind<T>(string s)
		{
			return BeginFind(typeof(T), s, new ObjectMapperOptions(), null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(Type type, string s, ObjectMapperOptions options)
		{
			return BeginFind(type, s, options, null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind<T>(string s, ObjectMapperOptions options)
		{
			return BeginFind(typeof(T), s, options, null, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="onMatchCallback">Callback invoked on every matched object. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(Type type, string s, ObjectMapperOptions options, Func<object, bool> onMatchCallback)
		{
			return BeginFind(type, s, options, onMatchCallback, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="onMatchCallback">Callback invoked on every matched object. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind<T>(string s, ObjectMapperOptions options, Func<object, bool> onMatchCallback)
		{
			return BeginFind(typeof(T), s, options, onMatchCallback, null, null);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <typeparam name="T">Generic type to find. This must be a matchable class.</typeparam>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="onMatchCallback">Callback invoked on every matched object. This value can be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind<T>(string s, ObjectMapperOptions options
			, Func<object, bool> onMatchCallback, AsyncCallback callback, object obj)
		{
			return BeginFind(typeof(T), s, options, onMatchCallback, callback, obj);
		}

		/// <summary>
		/// Performs an asynchronous find on a string with specific options.
		/// </summary>
		/// <param name="type">Type to map. This must be a matchable class. This value cannot be null.</param>
		/// <param name="s">String to match. This value cannot be null.</param>
		/// <param name="options">Options to control the mapping process. This value cannot be null.</param>
		/// <param name="onMatchCallback">Callback invoked on every matched object. This value can be null.</param>
		/// <param name="callback">Callback method passed to the async delegate. This value can be null.</param>
		/// <param name="obj">Custom data passed to the async delegate. This value can be null.</param>
		/// <returns>The async result.</returns>
		public static IAsyncResult BeginFind(Type type, string s, ObjectMapperOptions options
			, Func<object, bool> onMatchCallback, AsyncCallback callback, object obj)
		{
			if (type == null)
				throw new ArgumentNullException("type", "type cannot be null");
			if (s == null)
				throw new ArgumentNullException("s", "string cannot be null");
			if (options == null)
				throw new ArgumentNullException("options", "options cannot be null");

			var d = new Func<ObjectPattern, string, ObjectMapperOptions, Func<object, bool>, object[]>(FindAll);
			return new FindMapAsyncResult(d, d.BeginInvoke(new ObjectPattern(type), s, options, onMatchCallback, callback, obj));
		}

		/// <summary>
		/// Wait for and return the result of an asynchronous Map operation.
		/// </summary>
		/// <param name="result">The async object returned by the <b>BeginFind</b> method.</param>
		/// <returns>The mapped instance.</returns>
		public static object[] EndFind(IAsyncResult result)
		{
			var mapAsyncResult = result as FindMapAsyncResult;
			if (mapAsyncResult == null)
				throw new ArgumentException("Invalid async result", "result");
			return mapAsyncResult.EndFind();
		}

		private static IEnumerable<T> Find<T>(Type type, string s, ObjectMapperOptions options)
		{
			return Find<T>(new ObjectPattern(type), s, options);
		}

		private static object[] FindAll(ObjectPattern pattern, string s, ObjectMapperOptions options, Func<object, bool> onMatchCallback)
		{
			var items = new List<object>();
			foreach (var item in Find<object>(pattern, s, options))
			{
				if (onMatchCallback != null)
				{
					if (!onMatchCallback(item))
						break;
				}
				items.Add(item);
			}
			return items.ToArray();
		}

		private static IEnumerable<T> Find<T>(ObjectPattern pattern, string s, ObjectMapperOptions options)
		{
			var context = GetContext(options, s, pattern);

			while (!context.IsEos)
			{
				var branch = context.Branch();
				var m = pattern.Match(branch);

				if (m.Success)
				{
					context.Commit(branch);
					yield return (T)m.Value;
				}

				context.NextCharacter();
			}
		}
	}
}
